Skip to content

perf(payments): add indexes for payments list filters - #6341

Draft
sarkissianraffi wants to merge 1 commit into
mainfrom
perf/payments-list-filters-indexes
Draft

sarkissianraffi wants to merge 1 commit into
mainfrom
perf/payments-list-filters-indexes

Conversation

@sarkissianraffi

Copy link
Copy Markdown
Contributor

Adds the indexes the payments list filters (#6325) rely on for large organizations. Migration-only PR: no application code. Deploy this before #6325.

Indexes

table index serves
payment_receipts index_payment_receipts_on_organization_id_lower_number on (organization_id, lower(number)) exact, case-insensitive receipt_number filter
invoices index_invoices_on_organization_id_lower_number on (organization_id, lower(number)) exact, case-insensitive invoice_number filter (both payable paths)
payments index_payments_on_org_pending_processing_created_at on (organization_id, payable_payment_status, created_at DESC, id) WHERE payable_payment_status IN ('pending', 'processing') rare payment_status values, pre-sorted for the list. Partial on purpose: the planner cannot pick it for succeeded/failed, which the cursor index already serves

Same shape as index_invoices_on_organization_id_lower_purchase_order_number. All builds use algorithm: :concurrently, if_not_exists: true, one migration per table.

Build cost, synthetic dataset

Measured on a synthetic dataset of ~6.5M payments / 5.9M invoices / 3.5M receipts (see script/perf/payments_filters/ on the filters branch). Figures are from that dataset, not from production.

index rows build time (CONCURRENTLY, idle machine) size
payment_receipts (organization_id, lower(number)) 3.5M 4.2 s 107 MB
invoices (organization_id, lower(number)) 5.9M 12.7 s 382 MB
payments partial pending/processing 6.5M (135k indexed) 3.8 s 11 MB

Rejected after measurement (details in the internal doc): a full (organization_id, payable_payment_status, created_at DESC, id) index (423 MB, the planner takes it for succeeded and the count regresses), (organization_id, amount_cents) (same regression on common ranges), and an expression index on the payment method jsonb (never used while the saved-method fallback branch exists).

Deploying

  • config/database.yml applies LAGO_DATABASE_STATEMENT_TIMEOUT to every session, including db:migrate. Run the migration with the statement timeout unset (or above the expected build time). A short LAGO_DATABASE_LOCK_TIMEOUT is fine: CREATE INDEX CONCURRENTLY takes a ShareUpdateExclusiveLock only.
  • A concurrent build that is cancelled or fails leaves an INVALID index behind. Check after the deploy and drop/retry if any row comes back:
    SELECT indexrelid::regclass FROM pg_index WHERE NOT indisvalid;
  • Rollback: DROP INDEX CONCURRENTLY <name>, no application change needed.
  • After the filters PR ships, confirm the indexes are used: SELECT indexrelname, idx_scan FROM pg_stat_user_indexes WHERE indexrelname LIKE '%lower_number%'.

Performance analysis: internal document "Payments list filters: performance analysis" (Raffi; not public). All figures above come from the synthetic dataset built by script/perf/payments_filters/generate.rb on #6325.

Supports the payments list filters for large organizations:
- payment_receipts (organization_id, lower(number)) for receipt_number
- invoices (organization_id, lower(number)) for invoice_number
- payments (organization_id, payable_payment_status, created_at DESC, id)
  partial on pending/processing for the rare payment_status values

All builds are concurrent. To be deployed before the filters change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant